# -*- coding: utf-8 -*-
# @Time    : 2024/7/18 10:08
# @Author  : yujiahao
# @File    : 41_fastapi_sql_main.py
# @description:SQL (关系型) 数据库 主FastAPI应用程序(集成一下其他的部分)，这个案例还算可以综合了前面参数说明，这里是以MySQL为准
import importlib

from fastapi import Depends, FastAPI, HTTPException, Request, Response
from sqlalchemy.orm import Session

# from . import crud, models, schemas
# from . import database


# 动态导入模块，使用绝对路径 ，这里这样的话直接点不到对应的方法
database = importlib.import_module('com.yujiahao.my_fastapi.37_fastapi_sql_database')
models = importlib.import_module('com.yujiahao.my_fastapi.38_fastapi_sql_models')
schemas = importlib.import_module('com.yujiahao.my_fastapi.39_fastapi_sql_schemas')
crud = importlib.import_module('com.yujiahao.my_fastapi.40_fastapi_sql_crud')

# todo 创建数据库表
"""
最终用的是这个方法：declarative_base 是 SQLAlchemy 提供的一个函数，用于创建一个基类，这个基类会包含所有 ORM 模型的元数据。
通过继承这个基类，可以定义具体的 ORM 模型。
    - models.Base 是从 my_fastapi.38_fastapi_sql_models 模块中导入的 Base。
    - metadata 包含了所有继承自 Base 的 ORM 模型的元数据。
    - create_all 方法会在数据库中创建所有在 metadata 中定义的表。
    - bind=database.engine 指定了数据库引擎，通常是在 my_fastapi.37_fastapi_sql_database 模块中创建的。
    
这种方法适用于开发阶段，但在生产环境中，通常使用 Alembic 来管理数据库迁移。

Alembic 注意：

通常你可能会使用 Alembic 来进行格式化数据库（创建表等）。

而且您还可以将 Alembic 用于“迁移”（这是它的主要工作）。

“迁移”是每当您更改 SQLAlchemy 模型的结构、添加新属性等以在数据库中复制这些更改、添加新列、新表等时所需的一组步骤。

您可以在 Full Stack FastAPI Template（https://fastapi.tiangolo.com/zh/project-generation/） 的模板中找到一个 FastAPI 项目中的 Alembic 示例。具体在 alembic 代码目录中。

"""
models.Base.metadata.create_all(bind=database.engine)

app = FastAPI()


# todo 创建依赖项
#  使用前面database里面的文件中创建的SessionLocal来创建依赖项。
# 我们需要每个请求有一个独立的数据库会话/连接（SessionLocal），在整个请求中使用相同的会话，然后在请求完成后关闭它。
# 然后将为下一个请求创建一个新会话。


# todo ① 直接使用yield创建(推荐)
# 为此，我们将创建一个包含 yield 的依赖项，正如前面关于依赖项中 yield 的部分中所解释的那样。
# 我们的依赖项将创建一个新的 SQLAlchemy SessionLocal，它将在单个请求中使用，然后在请求完成后关闭它。

# def get_db():
#     db = database.SessionLocal()
#     '''
#     我们将SessionLocal()请求的创建和处理放在一个try块中。
#     然后我们在finally块中关闭它。
#     通过这种方式，我们确保数据库会话在请求后始终关闭。即使在处理请求时出现异常。
#     但是您不能从退出代码中引发另一个异常（在yield之后）。可以查阅fastapi中依赖和异常
#     '''
#     try:
#         yield db
#     finally:
#         db.close()


# todo ② 可以使用中间件替代数据库会话
# 如果不能使用带有yield的依赖项。例如，如果你没有使用Python 3.7并且不能安装上面提到的Python 3.6的“backports”
# 你可以使用类似的方法在“中间件”中设置会话。
# “中间件”基本上是一个对每个请求都执行的函数，其中一些代码在端点函数之前执行，另一些代码在端点函数之后执行。
# 另外说一句：无论什么方法，只要传入request就都能拿到这个中间件处理的，前提有中间件的情况。例如：
# 中间件设置了 request.state.db，那么在任何路径操作函数中，只要传入 Request 对象，您都可以通过 request.state.db 访问到这个数据库会话。


# 我们要添加的中间件（只是一个函数）将为每个请求创建一个新的 SQLAlchemySessionLocal，将其添加到请求中，然后在请求完成后关闭它。
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
    """
    关于 request.state:

    1. `request.state` 是每个 `Request` 对象的属性。它用于存储附加到请求本身的任意对象，例如本例中的数据库会话。
       您可以在 Starlette 的关于 `Request.state` 的文档中了解更多信息。

    2. 在这种情况下，它帮助我们确保在整个请求中使用单个数据库会话，然后在中间件中关闭它。

    使用 yield 依赖项与使用中间件的区别:

    1. 中间件需要更多的代码并且更复杂一些。
    2. 中间件必须是一个 `async` 函数。
       - 如果其中有代码必须“等待”网络，它可能会在那里“阻止”您的应用程序并稍微降低性能。
       - 尽管这里的 SQLAlchemy 工作方式可能不是很成问题，但如果您向等待大量 I/O 的中间件添加更多代码，则可能会出现问题。
    3. 每个请求都会运行一个中间件，这将为每个请求创建一个连接，即使处理该请求的路径操作不需要数据库。

    Tip:
    - 最好使用带有 `yield` 的依赖项，如果这足够满足用例需求。

    Info:
    - 带有 `yield` 的依赖项是最近刚加入 FastAPI 中的。

    【比较和推荐】

    使用中间件的场景
        - 全局一致性：如果您的应用需要在所有请求中一致地管理某种资源（如数据库会话），中间件是一个很好的选择。
        - 统一管理：对于需要在请求的整个生命周期中管理的资源，中间件提供了统一的入口和出口。
    使用 yield 的依赖项的场景
        - 特定路径操作：如果您只需要在特定的路径操作中管理资源，使用 yield 的依赖项更加合适。
        - 简洁性和可读性：对于简单的依赖管理，yield 的依赖项更加简洁和易读。
    结论
        - 中间件：适用于全局资源管理，特别是当您需要在每个请求中执行相同的初始化和清理逻辑时。
        - yield 的依赖项：适用于特定路径操作的资源管理，提供了更细粒度的控制和简洁的代码。
    """

    response = Response("Internal server error", status_code=500)
    try:
        request.state.db = database.SessionLocal()
        response = await call_next(request)
    finally:
        request.state.db.close()
    return response


# 这里通过中间件获取连接
def get_db(request: Request):
    return request.state.db


# todo 操作数据库表
"""
1. 在依赖项中，我们利用 `yield` 在每个请求之前创建数据库会话，并在请求结束后关闭它。
   这样，我们可以在路径操作函数中创建需要的依赖，从而直接获取会话。

2. 在路径操作函数内部，我们可以直接调用 CRUD 函数（如 `crud.get_user`）并使用该会话进行数据库操作。

3. 请注意，返回的值是 SQLAlchemy 模型或 SQLAlchemy 模型列表。
   但是由于所有路径操作的 `response_model` 都使用 Pydantic 模型并启用了 `orm_mode`，
   因此 Pydantic 模型中声明的数据将从 SQLAlchemy 模型中提取并返回给客户端，同时进行所有正常的过滤和验证。

4. `response_model` 应当是标准的 Python 类型，例如 `List[schemas.Item]`。
   由于其内容（如 `List` 的参数）是启用了 `orm_mode` 的 Pydantic 模型，
   因此数据将被正常检索并返回给客户端，这样就没有问题。
"""


# 当在路径操作函数中使用依赖项时，我们直接从 SQLAlchemy 导入 `Session` 类型来声明它。（下同）
# 这有助于在路径操作函数中提供更好的编辑器支持，因为编辑器将知道 `db` 参数的类型是 `Session`。
# 这几个还算是一个比较综合前面的说明参数和类型的，如果加上token就更好了
@app.post("/users/", response_model=schemas.User, summary="创建新用户",
          description="使用提供的电子邮件和密码创建一个新用户。")
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="电子邮件已被注册")
    return crud.create_user(db=db, user=user)


@app.get("/users/", response_model=list[schemas.User], summary="读取用户列表", description="检索用户列表，支持分页。")
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = crud.get_users(db, skip=skip, limit=limit)
    return users


@app.get("/users/{user_id}", response_model=schemas.User, summary="读取用户信息",
         description="通过用户ID检索用户信息。如果未找到用户，则返回404错误。")
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="用户未找到")
    return db_user


@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
        user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
    return crud.create_user_item(db=db, item=item, user_id=user_id)


@app.get("/items/", response_model=list[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    items = crud.get_items(db, skip=skip, limit=limit)
    return items


"""
关于 def 对比 async def:

1. 在路径操作函数和依赖项中，我们使用 SQLAlchemy 模型与外部数据库进行通信。这需要一些“等待时间”。

2. 由于 SQLAlchemy 不具有直接使用 `await` 的兼容性，因此不能使用如下代码：
   user = await db.query(User).first()
   相反，我们应该使用：
   user = db.query(User).first()

3. 因此，我们应该声明路径操作函数和依赖项为普通的 `def`，而不是 `async def`，例如：
   @app.get("/users/{user_id}", response_model=schemas.User)
   def read_user(user_id: int, db: Session = Depends(get_db)):
       db_user = crud.get_user(db, user_id=user_id)
       ...
"""
